#include <limits.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include "config.h"

#include "psd_auth.h"
#include "psd_conn.h"
#include "psd_config.h"
#include "psd_logger.h"
#include "psd_tpool.h"
#include "psd_measurement.h"
#include "psd_conn_tcp.h"
#include "psd_protocols.h"
#include "psd_modules.h"

//#ifdef HAVE_NETFILTER
#include <linux/netfilter_ipv4.h>
//#else
//#ifdef HAVE_PF
// put the relevant info here...
//#endif
//#endif

#include "compat.h"

static int psd_frontend_transparent_init();
static int psd_frontend_connection_handler(psdListener *listener, psdConn *conn, void *arg);
static void *psd_handle_transparent_conn(void *arg);

static psdModule psd_transparent_module = {
	.desc = "Transparent Phoebus Handler Module",
	.dependencies = "tcp",
	.init = psd_frontend_transparent_init
};

int psd_transparent_port = 5008;
int psd_transparent_send_bufsize = 0;
int psd_transparent_recv_bufsize = 0;
int psd_transparent_send_timeout = 0;
int psd_transparent_recv_timeout = 0;

psdModule *module_info() {
	return &psd_transparent_module;
}

int psd_sess_add_hop(psdSess *sess, xspHop *hop) {
	xspHop **new_list;
        int new_count;

        new_count = sess->child_count + 1;

        new_list = (xspHop **) realloc(sess->child, sizeof(xspHop *) * new_count);
        if (!new_list)
                return -1;

        sess->child = new_list;

        sess->child[sess->child_count] = hop;
	sess->child_count++;

	hop->session = sess;

        return 0;
}

int psd_frontend_transparent_init() {
	psdSettings *settings;
	psdListener *listener;
	int val;

	settings = psd_settings_alloc();
	if (!settings) {
		psd_err(0, "Couldn't allocate listener settings");
		goto error_exit;
	}

	if (psd_depot_settings_get_bool("transparent", "disabled", &val) == 0) {
		if (val) {
			psd_info(0, "Transparent module disabled");
			return 0;
		}
	}

	if (psd_depot_settings_get_int("transparent", "port", &val) == 0) {
		psd_settings_set_int_2(settings, "tcp", "port", val);
	}
	
	if (psd_depot_settings_get_int("transparent", "send_bufsize", &val) == 0) {
		psd_settings_set_int_2(settings, "tcp", "send_bufsize", val);
	}

	if (psd_depot_settings_get_int("transparent", "recv_bufsize", &val) == 0) {
		psd_settings_set_int_2(settings, "tcp", "recv_bufsize", val);
	}

	if (psd_depot_settings_get_int("transparent", "send_timeout", &val) == 0) {
		psd_settings_set_int_2(settings, "tcp", "send_timeout", val);
	}

	if (psd_depot_settings_get_int("transparent", "recv_timeout", &val) == 0) {
		psd_settings_set_int_2(settings, "tcp", "recv_timeout", val);
	}

	psd_info(0, "Setting up listener for transparent Phoebus module");

	if ((listener = psd_protocol_setup_listener("transparent", "tcp", settings, 0, psd_frontend_connection_handler, NULL)) == NULL) {
		psd_err(0, "Couldn't setup listener for transparent Phoebus module");
		goto error_exit_settings;
	}

	if (psd_listener_start(listener) != 0) {
		psd_err(0, "Couldn't start listener for transparent Phoebus module");
		goto error_exit_listener;
	}

	return 0;

error_exit_listener:
	psd_listener_free(listener);
error_exit_settings:
	psd_settings_free(settings);
error_exit:
	return -1;
}

static int psd_frontend_connection_handler(psdListener *listener, psdConn *conn, void *arg) {
	int retval;

	retval = psd_tpool_exec(psd_handle_transparent_conn, conn);

	return retval;
}

void *psd_handle_transparent_conn(void *arg) {
	psdConn *new_conn = (psdConn *) arg;
	psdSess *sess;
	xspHop *hop;
	struct sockaddr_storage sa;
	SOCKLEN_T sa_size = sizeof(struct sockaddr_storage);
	psdConn_tcpData *tcp_data;
	psdCreds *credentials;
	int child_fd;
	char **error_msgs = NULL;

	sess = psd_alloc_sess();
	if (!sess) {
		psd_err(5, "psd_alloc_sess() failed: %s", strerror(errno));
		goto error_exit;
	}

	psd_session_get_ref(sess);

	// generate a random session id
	gen_rand_hex(sess->id, 2*XSP_SESSIONID_LEN+1);

	hop = xsp_alloc_hop();
	if (!hop) {
		psd_err(5, "xsp_alloc_hop() failed: %s", strerror(errno));
		free(sess);
		goto error_exit2;
	}
	
	hop->session = (xspSess *) sess;

	tcp_data = new_conn->conn_private;
	child_fd = tcp_data->sd;

	if (psd_authenticate_connection(new_conn, "ANON", &credentials) != 0) {
		psd_err(0, "Authentication failed.");
		goto error_exit;
	}
	
	// copy the original address in here:
	if (getsockopt(child_fd, SOL_IP, SO_ORIGINAL_DST, &sa, &sa_size) != 0) {
		psd_err(5, "Couldn't get the original destination"); 
		goto error_exit3;
	}

	if (xsp_sa2hopid_r((struct sockaddr *) &sa, sizeof(sa), hop->hop_id, sizeof(hop->hop_id), 0) == NULL) {
		psd_err(5, "Couldn't convert destination to hop id");
		goto error_exit3;
	}
	
	if (psd_sess_add_hop(sess, hop) != 0) {
		psd_err(5, "Error adding \"%s\" to session", hop->hop_id);
		goto error_exit3;
	}

	LIST_INSERT_HEAD(&sess->parent_conns, new_conn, sess_entries);

	sess->credentials = credentials;
	psd_session_set_user(sess, strdup(credentials->get_user(credentials)));

        psd_info(0, "new user: \"%s\"(%s) from \"%s\"",
		 psd_session_get_user(sess),
		 credentials->get_email(credentials),
		 credentials->get_institution(credentials));

	gettimeofday(&sess->start_time, NULL);

	if (psd_setup_session(sess, &error_msgs) < 0) {
		psd_err(5, "Couldn't setup sessions");
		goto error_exit4;
	}

	if (LIST_EMPTY(&sess->child_conns)) {
		psd_err(5, "No one to send to");
		goto error_exit4;
	}

	// XXX: we're going to need something more complex for transparent sessions... sigh
	if (psd_session_main_loop(sess)) {
		psd_err(5, "Error in session main loop");
		goto error_exit4;
	}

	gettimeofday(&sess->end_time, NULL);

	// if we get here, IO has finished for this session
	psd_info(5, "session finished: %s", psd_session_get_id(sess));

	psd_session_finalize(sess);

	psd_session_put_ref(sess);

	return NULL;

error_exit4:
	// XXX: need to close ALL the sessions
	psd_end_session(sess);
error_exit3:
	return NULL;
error_exit2:
error_exit:
	psd_conn_shutdown(new_conn, (PSD_SEND_SIDE | PSD_RECV_SIDE));
	return NULL;
}
